Türkçe

JavaScript'te gerçek çoklu iş parçacığının kilidini açın. Bu kapsamlı kılavuz, SharedArrayBuffer, Atomics, Web Workers ve yüksek performanslı web uygulamaları için güvenlik gereksinimlerini ele almaktadır.

JavaScript SharedArrayBuffer: Web'de Eşzamanlı Programlamaya Derinlemesine Bir Bakış

Onlarca yıldır JavaScript'in tek iş parçacıklı (single-threaded) doğası, hem basitliğinin bir kaynağı hem de önemli bir performans darboğazı olmuştur. Olay döngüsü (event loop) modeli, çoğu arayüz odaklı görev için harika çalışır, ancak hesaplama açısından yoğun işlemlerle karşılaştığında zorlanır. Uzun süren hesaplamalar tarayıcıyı dondurarak sinir bozucu bir kullanıcı deneyimi yaratabilir. Web Workers, betiklerin arka planda çalışmasına izin vererek kısmi bir çözüm sunsa da, kendi büyük sınırlamalarıyla birlikte geldiler: verimsiz veri iletişimi.

İşte bu noktada SharedArrayBuffer (SAB), web'de iş parçacıkları arasında gerçek, düşük seviyeli bellek paylaşımını sunarak oyunu temelden değiştiren güçlü bir özellik olan devreye giriyor. Atomics nesnesiyle eşleştirilen SAB, doğrudan tarayıcıda yeni bir yüksek performanslı, eşzamanlı uygulamalar çağının kilidini açar. Ancak, büyük güç büyük sorumluluk ve karmaşıklık getirir.

Bu kılavuz sizi JavaScript'te eşzamanlı programlama dünyasına derinlemesine bir yolculuğa çıkaracak. Neden buna ihtiyacımız olduğunu, SharedArrayBuffer ve Atomics'in nasıl çalıştığını, ele almanız gereken kritik güvenlik hususlarını ve başlamanıza yardımcı olacak pratik örnekleri inceleyeceğiz.

Eski Dünya: JavaScript'in Tek İş Parçacıklı Modeli ve Sınırlamaları

Çözümü takdir edebilmek için önce sorunu tam olarak anlamalıyız. Bir tarayıcıdaki JavaScript yürütmesi, geleneksel olarak "ana iş parçacığı" veya "UI iş parçacığı" olarak adlandırılan tek bir iş parçacığında gerçekleşir.

Olay Döngüsü (Event Loop)

Ana iş parçacığı her şeyden sorumludur: JavaScript kodunuzu yürütmek, sayfayı oluşturmak, kullanıcı etkileşimlerine (tıklamalar ve kaydırmalar gibi) yanıt vermek ve CSS animasyonlarını çalıştırmak. Bu görevleri, sürekli olarak bir mesaj (görev) kuyruğunu işleyen bir olay döngüsü kullanarak yönetir. Bir görevin tamamlanması uzun sürerse, tüm kuyruğu engeller. Başka hiçbir şey olamaz—kullanıcı arayüzü donar, animasyonlar takılır ve sayfa yanıt vermez hale gelir.

Web Workers: Doğru Yönde Bir Adım

Web Workers bu sorunu azaltmak için tanıtıldı. Bir Web Worker, esasen ayrı bir arka plan iş parçacığında çalışan bir betiktir. Ağır hesaplamaları bir worker'a yükleyerek ana iş parçacığını kullanıcı arayüzünü yönetmek için serbest bırakabilirsiniz.

Ana iş parçacığı ile bir worker arasındaki iletişim postMessage() API'si aracılığıyla gerçekleşir. Veri gönderdiğinizde, bu veri yapılandırılmış klon algoritması tarafından işlenir. Bu, verinin serileştirildiği, kopyalandığı ve ardından worker'ın bağlamında seriden çıkarıldığı anlamına gelir. Etkili olsa da, bu sürecin büyük veri setleri için önemli dezavantajları vardır:

Tarayıcıda bir video düzenleyici hayal edin. Saniyede 60 kez işlemek için bir video karesinin tamamını (birkaç megabayt olabilir) bir worker'a ileri geri göndermek, fahiş derecede pahalı olurdu. İşte SharedArrayBuffer tam olarak bu sorunu çözmek için tasarlandı.

Oyunun Kurallarını Değiştiren: SharedArrayBuffer ile Tanışın

Bir SharedArrayBuffer, bir ArrayBuffer'a benzer şekilde, sabit uzunlukta bir ham ikili veri arabelleğidir. Kritik fark, bir SharedArrayBuffer'ın birden fazla iş parçacığı (örneğin, ana iş parçacığı ve bir veya daha fazla Web Worker) arasında paylaşılabilmesidir. postMessage() kullanarak bir SharedArrayBuffer "gönderdiğinizde", bir kopya göndermiyorsunuz; aynı bellek bloğuna bir referans gönderiyorsunuz.

Bu, bir iş parçacığı tarafından arabellek verilerinde yapılan herhangi bir değişikliğin, ona referansı olan diğer tüm iş parçacıkları tarafından anında görülebildiği anlamına gelir. Bu, maliyetli kopyala-serileştir adımını ortadan kaldırarak neredeyse anlık veri paylaşımını mümkün kılar.

Şöyle düşünün:

Paylaşılan Belleğin Tehlikesi: Yarış Durumları (Race Conditions)

Anlık bellek paylaşımı güçlüdür, ancak aynı zamanda eşzamanlı programlama dünyasından klasik bir sorunu da beraberinde getirir: yarış durumları.

Bir yarış durumu, birden fazla iş parçacığı aynı paylaşılan veriye aynı anda erişmeye ve değiştirmeye çalıştığında ve nihai sonucun, hangi sırayla çalıştıklarının öngörülemeyen düzenine bağlı olduğunda ortaya çıkar. Bir SharedArrayBuffer'da saklanan basit bir sayacı düşünün. Hem ana iş parçacığı hem de bir worker onu artırmak istiyor.

  1. A İş Parçacığı mevcut değeri okur, değer 5'tir.
  2. A İş Parçacığı yeni değeri yazamadan, işletim sistemi onu duraklatır ve B İş Parçacığı'na geçer.
  3. B İş Parçacığı mevcut değeri okur, değer hala 5'tir.
  4. B İş Parçacığı yeni değeri (6) hesaplar ve belleğe geri yazar.
  5. Sistem tekrar A İş Parçacığı'na döner. B İş Parçacığı'nın bir şey yaptığını bilmez. Kaldığı yerden devam eder, yeni değerini (5 + 1 = 6) hesaplar ve 6'yı belleğe geri yazar.

Sayaç iki kez artırılmasına rağmen, nihai değer 7 değil, 6'dır. İşlemler atomik değildi—kesintiye uğrayabilirlerdi ve bu da veri kaybına yol açtı. İşte tam da bu yüzden bir SharedArrayBuffer'ı onun hayati ortağı olan Atomics nesnesi olmadan kullanamazsınız.

Paylaşılan Belleğin Koruyucusu: Atomics Nesnesi

Atomics nesnesi, SharedArrayBuffer nesneleri üzerinde atomik işlemler gerçekleştirmek için bir dizi statik metot sağlar. Bir atomik işlemin, başka herhangi bir işlem tarafından kesintiye uğratılmadan tamamen gerçekleştirileceği garanti edilir. Ya tamamen gerçekleşir ya da hiç gerçekleşmez.

Atomics kullanmak, paylaşılan bellek üzerindeki oku-değiştir-yaz işlemlerinin güvenli bir şekilde yapılmasını sağlayarak yarış durumlarını önler.

Temel Atomics Metotları

Atomics tarafından sağlanan en önemli metotlardan bazılarına bakalım.

Senkronizasyon: Basit İşlemlerin Ötesi

Bazen güvenli okuma ve yazmadan daha fazlasına ihtiyacınız olur. İş parçacıklarının koordine olması ve birbirini beklemesi gerekir. Yaygın bir anti-desen, bir iş parçacığının sıkı bir döngüde oturup bir bellek konumunda değişiklik olup olmadığını sürekli kontrol ettiği "meşgul bekleme"dir. Bu, CPU döngülerini boşa harcar ve pil ömrünü tüketir.

Atomics, wait() ve notify() ile çok daha verimli bir çözüm sunar.

Hepsini Bir Araya Getirmek: Pratik Bir Kılavuz

Şimdi teoriyi anladığımıza göre, SharedArrayBuffer kullanarak bir çözüm uygulama adımlarını gözden geçirelim.

Adım 1: Güvenlik Ön Koşulu - Cross-Origin İzolasyonu

Bu, geliştiriciler için en yaygın engeldir. Güvenlik nedenleriyle, SharedArrayBuffer yalnızca cross-origin isolated (çapraz köken yalıtılmış) durumdaki sayfalarda kullanılabilir. Bu, Spectre gibi spekülatif yürütme güvenlik açıklarını azaltmak için bir güvenlik önlemidir. Bu açıklar, potansiyel olarak (paylaşılan bellek sayesinde mümkün olan) yüksek çözünürlüklü zamanlayıcıları kullanarak kökenler arasında veri sızdırabilir.

Cross-origin izolasyonunu etkinleştirmek için, web sunucunuzu ana belgeniz için iki özel HTTP başlığı gönderecek şekilde yapılandırmanız gerekir:


Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Bunu kurmak zor olabilir, özellikle de gerekli başlıkları sağlamayan üçüncü taraf betiklerine veya kaynaklarına güveniyorsanız. Sunucunuzu yapılandırdıktan sonra, tarayıcının konsolunda self.crossOriginIsolated özelliğini kontrol ederek sayfanızın yalıtılmış olup olmadığını doğrulayabilirsiniz. Bu değer true olmalıdır.

Adım 2: Arabelleği Oluşturma ve Paylaşma

Ana betiğinizde, SharedArrayBuffer'ı ve üzerinde Int32Array gibi bir TypedArray kullanarak bir "görünüm" oluşturursunuz.

main.js:


// Önce cross-origin izolasyonunu kontrol et!
if (!self.crossOriginIsolated) {
  console.error("Bu sayfa cross-origin isolated değil. SharedArrayBuffer kullanılamayacak.");
} else {
  // Bir adet 32-bit tamsayı için paylaşılan bir arabellek oluştur.
  const buffer = new SharedArrayBuffer(4);

  // Arabellek üzerinde bir görünüm oluştur. Tüm atomik işlemler görünüm üzerinde gerçekleşir.
  const int32Array = new Int32Array(buffer);

  // 0 indeksindeki değeri başlat.
  int32Array[0] = 0;

  // Yeni bir worker oluştur.
  const worker = new Worker('worker.js');

  // PAYLAŞILAN arabelleği worker'a gönder. Bu bir kopya değil, referans transferidir.
  worker.postMessage({ buffer });

  // Worker'dan gelen mesajları dinle.
  worker.onmessage = (event) => {
    console.log(`Worker tamamlandığını bildirdi. Nihai değer: ${Atomics.load(int32Array, 0)}`);
  };
}

Adım 3: Worker'da Atomik İşlemler Gerçekleştirme

Worker arabelleği alır ve artık üzerinde atomik işlemler gerçekleştirebilir.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker paylaşılan arabelleği aldı.");

  // Birkaç atomik işlem yapalım.
  for (let i = 0; i < 1000000; i++) {
    // Paylaşılan değeri güvenli bir şekilde artır.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker artırma işlemini bitirdi.");

  // Bittiğimizi ana iş parçacığına bildir.
  self.postMessage({ done: true });
};

Adım 4: Daha Gelişmiş Bir Örnek - Senkronizasyon ile Paralel Toplama

Daha gerçekçi bir sorunu ele alalım: çok büyük bir sayı dizisini birden çok worker kullanarak toplamak. Verimli senkronizasyon için Atomics.wait() ve Atomics.notify() kullanacağız.

Paylaşılan arabelleğimizin üç bölümü olacak:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [durum, biten_workerlar, sonuc_dusuk, sonuc_yuksek]
  // Büyük toplamlar için taşmayı önlemek amacıyla sonuç için iki 32-bit tamsayı kullanıyoruz.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 tamsayı
  const sharedArray = new Int32Array(sharedBuffer);

  // İşlemek için rastgele veri oluştur
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Worker'ın veri parçası için paylaşılmayan bir görünüm oluştur
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // Bu kopyalanır
    });
  }

  console.log('Ana iş parçacığı şimdi worker\'ların bitirmesini bekliyor...');

  // 0 indeksindeki durum bayrağının 1 olmasını bekle
  // Bu, bir while döngüsünden çok daha iyidir!
  Atomics.wait(sharedArray, 0, 0); // sharedArray[0] 0 ise bekle

  console.log('Ana iş parçacığı uyandırıldı!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`Nihai paralel toplam: ${finalSum}`);

} else {
  console.error('Sayfa cross-origin isolated değil.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Bu worker'ın parçası için toplamı hesapla
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Yerel toplamı atomik olarak paylaşılan toplama ekle
  Atomics.add(sharedArray, 2, localSum);

  // 'biten workerlar' sayacını atomik olarak artır
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // Eğer bu bitiren son worker ise...
  const NUM_WORKERS = 4; // Gerçek bir uygulamada bu değerin parametre olarak geçilmesi gerekir
  if (finishedCount === NUM_WORKERS) {
    console.log('Son worker bitirdi. Ana iş parçacığına bildiriliyor.');

    // 1. Durum bayrağını 1'e ayarla (tamamlandı)
    Atomics.store(sharedArray, 0, 1);

    // 2. 0 indeksinde bekleyen ana iş parçacığını uyar
    Atomics.notify(sharedArray, 0, 1);
  }
};

Gerçek Dünya Kullanım Alanları ve Uygulamaları

Bu güçlü ama karmaşık teknoloji gerçekte nerede bir fark yaratıyor? Büyük veri setleri üzerinde ağır, paralelleştirilebilir hesaplama gerektiren uygulamalarda üstün başarı gösterir.

Zorluklar ve Son Değerlendirmeler

SharedArrayBuffer dönüştürücü bir teknoloji olsa da, her derde deva değildir. Dikkatli kullanım gerektiren düşük seviyeli bir araçtır.

  1. Karmaşıklık: Eşzamanlı programlama herkesin bildiği gibi zordur. Yarış durumlarını ve kilitlenmeleri (deadlocks) ayıklamak inanılmaz derecede zor olabilir. Uygulama durumunuzun nasıl yönetildiği hakkında farklı düşünmeniz gerekir.
  2. Kilitlenmeler (Deadlocks): Bir kilitlenme, iki veya daha fazla iş parçacığı sonsuza dek engellendiğinde, her biri diğerinin bir kaynağı serbest bırakmasını beklediğinde meydana gelir. Karmaşık kilitleme mekanizmalarını yanlış uygularsanız bu durum ortaya çıkabilir.
  3. Güvenlik Yükü: Cross-origin izolasyon gereksinimi önemli bir engeldir. Gerekli CORS/CORP başlıklarını desteklemiyorlarsa üçüncü taraf hizmetleri, reklamlar ve ödeme ağ geçitleri ile entegrasyonları bozabilir.
  4. Her Sorun İçin Değil: Basit arka plan görevleri veya G/Ç işlemleri için, postMessage() ile geleneksel Web Worker modeli genellikle daha basit ve yeterlidir. Sadece büyük miktarda veri içeren, açıkça CPU'ya bağlı bir darboğazınız olduğunda SharedArrayBuffer'a başvurun.

Sonuç

SharedArrayBuffer, Atomics ve Web Workers ile birlikte, web geliştirme için bir paradigma kaymasını temsil eder. Tek iş parçacıklı modelin sınırlarını yıkarak, yeni bir sınıf güçlü, performanslı ve karmaşık uygulamaları tarayıcıya davet eder. Web platformunu, hesaplama açısından yoğun görevler için yerel uygulama geliştirmeyle daha eşit bir zemine yerleştirir.

Eşzamanlı JavaScript'e yolculuk, durum yönetimi, senkronizasyon ve güvenlik konularında titiz bir yaklaşım gerektiren zorlu bir süreçtir. Ancak web'de mümkün olanın sınırlarını zorlamak isteyen geliştiriciler için -gerçek zamanlı ses sentezinden karmaşık 3D renderlamaya ve bilimsel hesaplamaya kadar- SharedArrayBuffer'da ustalaşmak artık sadece bir seçenek değil; yeni nesil web uygulamalarını oluşturmak için temel bir beceridir.

JavaScript SharedArrayBuffer: Web'de Eşzamanlı Programlamaya Derinlemesine Bir Bakış | MLOG